<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<link rel="import" href="../lib/case-map.html">

<script>

  /**
   * Support for `hostAttributes` property.
   *
   *     hostAttributes: {
   *       'aria-role': 'button',
   *       tabindex: 0
   *     }
   *
   * `hostAttributes` is an object containing attribute names as keys and static values
   * to set to attributes when the element is created.
   *
   * Support for mapping attributes to properties.
   *
   * Properties that are configured in `properties` with a type are mapped
   * to attributes.
   *
   * A value set in an attribute is deserialized into the specified
   * data-type and stored into the matching property.
   *
   * Example:
   *
   *     properties: {
   *       // values set to index attribute are converted to Number and propagated
   *       // to index property
   *       index: Number,
   *       // values set to label attribute are propagated to index property
   *       label: String
   *     }
   *
   * Types supported for deserialization:
   *
   * - Number
   * - Boolean
   * - String
   * - Object (JSON)
   * - Array (JSON)
   * - Date
   *
   * This feature implements `attributeChanged` to support automatic
   * propagation of attribute values at run-time. If you override
   * `attributeChanged` be sure to call this base class method
   * if you also want the standard behavior.
   *
   * @class base feature: attributes
   */

  Polymer.Base._addFeature({

    // prototype time
    _addHostAttributes: function(attributes) {
      if (!this._aggregatedAttributes) {
        this._aggregatedAttributes = {};
      }
      if (attributes) {
        this.mixin(this._aggregatedAttributes, attributes);
      }
    },

    // instance time
    _marshalHostAttributes: function() {
      if (this._aggregatedAttributes) {
        this._applyAttributes(this, this._aggregatedAttributes);
      }
    },

    /* apply attributes to node but avoid overriding existing values */
    _applyAttributes: function(node, attr$) {
      for (var n in attr$) {
        // NOTE: never allow 'class' to be set in hostAttributes
        // since shimming classes would make it work
        // inconsisently under native SD
        if (!this.hasAttribute(n) && (n !== 'class')) {
          var v = attr$[n];
          this.serializeValueToAttribute(v, n, this);
        }
      }
    },

    _marshalAttributes: function() {
      this._takeAttributesToModel(this);
    },

    _takeAttributesToModel: function(model) {
      if (this.hasAttributes()) {
        for (var i in this._propertyInfo) {
          var info = this._propertyInfo[i];
          if (this.hasAttribute(info.attribute)) {
            this._setAttributeToProperty(model, info.attribute, i, info);
          }
        }
      }
    },

    _setAttributeToProperty: function(model, attribute, property, info) {
      // Don't deserialize back to property if currently reflecting
      if (!this._serializing) {
        property = (property || Polymer.CaseMap.dashToCamelCase(attribute));
        // fallback to property lookup
        // TODO(sorvell): check for _propertyInfo existence because of dom-bind
        info = info || (this._propertyInfo && this._propertyInfo[property]);
        if (info && !info.readOnly) {
          var v = this.getAttribute(attribute);
          model[property] = this.deserialize(v, info.type);
        }
      }
    },

    _serializing: false,

    /**
     * Serializes a property to its associated attribute.
     *
     * Generally users should set `reflectToAttribute: true` in the
     * `properties` configuration to achieve automatic attribute reflection.
     *
     * @method reflectPropertyToAttribute
     * @param {string} property Property name to reflect.
     * @param {*=} attribute Attribute name to reflect.
     * @param {*=} value Property value to refect.
     */
    reflectPropertyToAttribute: function(property, attribute, value) {
      this._serializing = true;
      value = (value === undefined) ? this[property] : value;
      this.serializeValueToAttribute(value,
        attribute || Polymer.CaseMap.camelToDashCase(property));
      this._serializing = false;
    },

    /**
     * Sets a typed value to an HTML attribute on a node.
     *
     * This method calls the `serialize` method to convert the typed
     * value to a string.  If the `serialize` method returns `undefined`,
     * the attribute will be removed (this is the default for boolean
     * type `false`).
     *
     * @method serializeValueToAttribute
     * @param {*} value Value to serialize.
     * @param {string} attribute Attribute name to serialize to.
     * @param {Element=} node Element to set attribute to (defaults to this).
     */
    serializeValueToAttribute: function(value, attribute, node) {
      var str = this.serialize(value);
      // TODO(kschaaf): Consider enabling under a flag
      // if (str && str.length > 250) {
      //   this._warn(this._logf('serializeValueToAttribute',
      //     'serializing long attribute values can lead to poor performance', this));
      // }
      node = node || this;
      if (str === undefined) {
        node.removeAttribute(attribute);
      } else {
        node.setAttribute(attribute, str);
      }
    },

    /**
     * Converts a string to a typed value.
     *
     * This method is called by Polymer when reading HTML attribute values to
     * JS properties.  Users may override this method on Polymer element
     * prototypes to provide deserialization for custom `type`s.  Note,
     * the `type` argument is the value of the `type` field provided in the
     * `properties` configuration object for a given property, and is
     * by convention the constructor for the type to deserialize.
     *
     * Note: The return value of `undefined` is used as a sentinel value to
     * indicate the attribute should be removed.
     *
     * @method deserialize
     * @param {string} value Attribute value to deserialize.
     * @param {*} type Type to deserialize the string to.
     * @return {*} Typed value deserialized from the provided string.
     */
    deserialize: function(value, type) {
      switch (type) {
        case Number:
          value = Number(value);
          break;

        case Boolean:
          value = (value != null);
          break;

        case Object:
          try {
            value = JSON.parse(value);
          } catch(x) {
            // allow non-JSON literals like Strings and Numbers
          }
          break;

        case Array:
          try {
            value = JSON.parse(value);
          } catch(x) {
            value = null;
            console.warn('Polymer::Attributes: couldn`t decode Array as JSON');
          }
          break;

        case Date:
          value = new Date(value);
          break;

        case String:
        default:
          break;
      }
      return value;
    },

    /**
     * Converts a typed value to a string.
     *
     * This method is called by Polymer when setting JS property values to
     * HTML attributes.  Users may override this method on Polymer element
     * prototypes to provide serialization for custom types.
     *
     * @method serialize
     * @param {*} value Property value to serialize.
     * @return {string} String serialized from the provided property value.
     */
    serialize: function(value) {
      /* eslint-disable no-fallthrough */
      switch (typeof value) {
        case 'boolean':
          return value ? '' : undefined;

        case 'object':
          if (value instanceof Date) {
            return value.toString();
          } else if (value) {
            try {
              return JSON.stringify(value);
            } catch(x) {
              return '';
            }
          }

        default:
          return value != null ? value : undefined;
      }
    }
    /* eslint-enable no-fallthrough */
  });

</script>
